#define _GNU_SOURCE
#include <X11/Xft/Xft.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/extensions/Xinerama.h>
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define SchemeNorm 0
#define SchemeSel 1
#define SchemeLast 2

static const char *colors[SchemeLast][2] = {
    [SchemeNorm] = {"#ffffff", "#222222"},
    [SchemeSel]  = {"#000000", "#ffff00"},
};

static const char *font_names[] = {
    "DejaVu Sans Mono:size=20:antialias=true:hinting=true",
    "Monaco:size=20:antialias=true:hinting=true",
    "Consolas:size=20:antialias=true:hinting=true",
    "monospace:size=20:antialias=true:hinting=true", NULL};

static unsigned int win_w = 1280, win_h = 720;
static int curr_offset = 0;
static int sel_idx = 0;
static int items_per_page = 0;
static char search_buf[256] = {0};
static int search_idx = 0;
static int searching = 0;
static int show_help = 0;

typedef struct {
  char text[1024];
  char path[1024];
  unsigned long long bytes;
} Entry;
static Entry *entries = NULL;
static int entry_count = 0;

static int compare_entries(const void *arg1, const void *arg2) {
  Entry *a = (Entry *)arg1;
  Entry *b = (Entry *)arg2;
  if (b->bytes > a->bytes) return 1;
  if (b->bytes < a->bytes) return -1;
  return 0;
}

unsigned long long size_str_to_bytes(const char *size_str) {
  char *endptr;
  double num = strtod(size_str, &endptr);
  unsigned long long bytes = 0;
  switch (tolower(*endptr)) {
  case 'k': bytes = num * 1024; break;
  case 'm': bytes = num * 1024 * 1024; break;
  case 'g': bytes = num * 1024 * 1024 * 1024; break;
  default: bytes = num; break;
  }
  return bytes;
}

void read_and_process_entries() {
  char line[1024];
  while (fgets(line, sizeof(line), stdin)) {
    line[strcspn(line, "\n")] = '\0';
    if (strlen(line) == 0) continue;

    char size_str[32], path[1024];
    if (sscanf(line, "%s %[^\n]", size_str, path) != 2) continue;
    if (strcmp(path, ".") == 0 || strcmp(path, "..") == 0) continue;

    unsigned long long bytes = size_str_to_bytes(size_str);
    entry_count++;
    entries = realloc(entries, entry_count * sizeof(Entry));
    if (!entries) {
      fprintf(stderr, "Error: Out of memory\n");
      exit(1);
    }
    Entry *e = &entries[entry_count - 1];
    strncpy(e->text, line, sizeof(e->text) - 1);
    strncpy(e->path, path, sizeof(e->path) - 1);
    e->bytes = bytes;
  }
  if (entry_count > 0) qsort(entries, entry_count, sizeof(Entry), compare_entries);
}

typedef struct {
  Display *dpy;
  unsigned int h;
  XftFont *xfont;
} Fnt;

enum { ColFg, ColBg };
typedef XftColor Clr;

typedef struct {
  unsigned int w, h;
  Display *dpy;
  int screen;
  Window root;
  Drawable drawable;
  GC gc;
  Clr *scheme;
  Fnt *font;
} Drw;

Drw *drw_create(Display *dpy, int screen, Window root) {
  Drw *drw = calloc(1, sizeof(Drw));
  if (!drw) return NULL;
  drw->dpy = dpy;
  drw->screen = screen;
  drw->root = root;
  drw->w = win_w;
  drw->h = win_h;
  drw->drawable = XCreatePixmap(dpy, root, drw->w, drw->h, DefaultDepth(dpy, screen));
  drw->gc = XCreateGC(dpy, root, 0, NULL);
  return drw;
}

Fnt *font_create(Drw *drw) {
  if (!drw) return NULL;
  Fnt *f = calloc(1, sizeof(Fnt));
  if (!f) return NULL;
  f->dpy = drw->dpy;
  XftFont *xfont = NULL;
  for (int i = 0; font_names[i]; i++) {
    xfont = XftFontOpenName(drw->dpy, drw->screen, font_names[i]);
    if (xfont) break;
  }
  if (!xfont) {
    fprintf(stderr, "Warning: Cannot load optimized font, use default monospace\n");
    xfont = XftFontOpenName(drw->dpy, drw->screen, "monospace:size=10");
    if (!xfont) {
      fprintf(stderr, "Error: Cannot load any font\n");
      free(f);
      return NULL;
    }
  }
  f->xfont = xfont;
  f->h = xfont->ascent + xfont->descent;
  return f;
}

void drw_clr_create(Drw *drw, Clr *dest, const char *name) {
  if (!drw || !dest) return;
  if (!XftColorAllocName(drw->dpy, DefaultVisual(drw->dpy, drw->screen),
                         DefaultColormap(drw->dpy, drw->screen), name, dest))
    fprintf(stderr, "Cannot allocate color: %s\n", name);
}

void drw_rect(Drw *drw, int x, int y, unsigned int w, unsigned int h, int filled) {
  if (!drw) return;
  XSetForeground(drw->dpy, drw->gc, drw->scheme[ColBg].pixel);
  if (filled) XFillRectangle(drw->dpy, drw->drawable, drw->gc, x, y, w, h);
  else XDrawRectangle(drw->dpy, drw->drawable, drw->gc, x, y, w - 1, h - 1);
}

void drw_text(Drw *drw, int x, int y, const char *text) {
  if (!drw || !text) return;
  char ascii_text[1024];
  unsigned int idx = 0;
  for (int i = 0; text[i] && idx < sizeof(ascii_text) - 1; i++) {
    if (text[i] >= 0x20 && text[i] <= 0x7E) ascii_text[idx++] = text[i];
  }
  ascii_text[idx] = '\0';

  XftDraw *d = XftDrawCreate(drw->dpy, drw->drawable,
                             DefaultVisual(drw->dpy, drw->screen),
                             DefaultColormap(drw->dpy, drw->screen));
  if (!d) return;
  int ty = y + (drw->font->h / 2) + (drw->font->xfont->ascent / 2);

  XGlyphInfo ext;
  XftTextExtentsUtf8(drw->dpy, drw->font->xfont, (XftChar8 *)ascii_text,
                     strlen(ascii_text), &ext);
  if ((unsigned int)ext.xOff > drw->w - 120) {
    char truncated[1024];
    strncpy(truncated, ascii_text, 30);
    truncated[30] = '\0';
    strcat(truncated, "...");
    XftDrawStringUtf8(d, &drw->scheme[ColFg], drw->font->xfont, x, ty,
                      (XftChar8 *)truncated, strlen(truncated));
  } else {
    XftDrawStringUtf8(d, &drw->scheme[ColFg], drw->font->xfont, x, ty,
                      (XftChar8 *)ascii_text, strlen(ascii_text));
  }

  XftDrawDestroy(d);
}

void drw_refresh(Drw *drw, Window win) {
  if (!drw) return;
  XCopyArea(drw->dpy, drw->drawable, win, drw->gc, 0, 0, drw->w, drw->h, 0, 0);
  XSync(drw->dpy, False);
}

void draw_list(Drw *drw, Clr *norm, Clr *sel) {
  if (!drw || !norm || !sel) return;

  int search_bar_h = searching ? drw->font->h : 0;
  int status_bar_h = drw->font->h;
  int list_h = drw->h - search_bar_h - status_bar_h;
  items_per_page = list_h / drw->font->h;
  if (items_per_page <= 0) items_per_page = 1;

  drw->scheme = norm;
  drw_rect(drw, 0, 0, drw->w, drw->h, 1);

  if (searching) {
    drw->scheme = sel;
    drw_rect(drw, 0, 0, drw->w, search_bar_h, 1);
    char search_prompt[512];
    snprintf(search_prompt, sizeof(search_prompt), "Search: /%s", search_buf);
    drw->scheme = sel;
    drw_text(drw, 10, 0, search_prompt);
  }

  int list_y_start = search_bar_h;
  int y = list_y_start;
  for (int i = curr_offset; i < entry_count && i < curr_offset + items_per_page; i++) {
    drw->scheme = (i == sel_idx) ? sel : norm;
    drw_rect(drw, 0, y, drw->w, drw->font->h, 1);
    drw_text(drw, 10, y, entries[i].text);
    y += drw->font->h;
  }

  char status[512];
  int total_pages = (entry_count + items_per_page - 1) / items_per_page;
  int curr_page = (curr_offset / items_per_page) + 1;
  snprintf(status, sizeof(status), "Loop Scroll | Page %d/%d | Total %d Items | Tab=Help | /=Search",
           curr_page, total_pages, entry_count);
  drw->scheme = sel;
  drw_rect(drw, 0, drw->h - status_bar_h, drw->w, status_bar_h, 1);
  drw->scheme = sel;
  drw_text(drw, 10, drw->h - status_bar_h, status);

  if (show_help) {
    drw->scheme = sel;
    drw_rect(drw, 50, 50, drw->w - 100, drw->h - 100, 1);
    drw->scheme = sel;
    const char *help_items[] = {
      "Help Panel",
      "==========",
      "  /        - Start search mode",
      "  ESC      - Exit search / Quit program",
      "  Tab      - Toggle this help panel",
      "  Single-click - Select item & Quit",
      "  Mouse wheel - Scroll pages",
      "  Backspace - Delete search character",
      "  + - * / =  - Supported in search",
      NULL
    };
    int help_y = 60;
    for (int i = 0; help_items[i]; i++) {
      drw_text(drw, 60, help_y, help_items[i]);
      help_y += drw->font->h;
    }
  }
}

void handle_mouse(Drw *drw, Clr *norm, Clr *sel, XButtonEvent *ev) {
  if (!drw || !norm || !sel || !ev) return;

  if (ev->button == 4) {
    sel_idx = (sel_idx == 0) ? entry_count - 1 : sel_idx - 1;
    curr_offset = sel_idx - (sel_idx % items_per_page);
    draw_list(drw, norm, sel);
    drw_refresh(drw, ev->window);
  } else if (ev->button == 5) {
    sel_idx = (sel_idx == entry_count - 1) ? 0 : sel_idx + 1;
    curr_offset = sel_idx - (sel_idx % items_per_page);
    draw_list(drw, norm, sel);
    drw_refresh(drw, ev->window);
  } else if (ev->type == ButtonPress) {
    int search_bar_h = searching ? drw->font->h : 0;
    int click_idx = curr_offset + ((ev->y - search_bar_h) / drw->font->h);
    if (click_idx >= 0 && click_idx < entry_count) {
      printf("%s\n", entries[click_idx].path);
      exit(0);
    }
  }
}

void handle_search(KeySym ks) {
  if (ks == XK_Escape) {
    searching = 0;
    memset(search_buf, 0, sizeof(search_buf));
    search_idx = 0;
    return;
  }
  if (ks == XK_BackSpace) {
    int len = strlen(search_buf);
    if (len > 0) search_buf[len - 1] = '\0';
    return;
  }
  char c = 0;
  if (isprint((int)ks)) {
    c = (char)ks;
  } else {
    switch (ks) {
      case XK_plus: c = '+'; break;
      case XK_minus: c = '-'; break;
      case XK_asterisk: c = '*'; break;
      case XK_slash: c = '/'; break;
      case XK_equal: c = '='; break;
      default: return;
    }
  }
  if (strlen(search_buf) < sizeof(search_buf) - 1) {
    search_buf[strlen(search_buf)] = c;
    search_buf[strlen(search_buf) + 1] = '\0';
  }
  if (strlen(search_buf) == 0) {
    searching = 0;
    return;
  }
  searching = 1;
  int start = search_idx;
  for (int i = 0; i < entry_count; i++) {
    int idx = (start + i) % entry_count;
    if (strstr(entries[idx].text, search_buf) != NULL) {
      sel_idx = idx;
      curr_offset = sel_idx - (sel_idx % items_per_page);
      search_idx = (idx + 1) % entry_count;
      break;
    }
  }
}

void event_loop(Display *dpy, Window win, Drw *drw, Clr *norm, Clr *sel) {
  if (!dpy || !drw || !norm || !sel) return;
  XEvent ev;
  while (1) {
    XNextEvent(dpy, &ev);
    switch (ev.type) {
    case Expose:
      if (ev.xexpose.count == 0) {
        draw_list(drw, norm, sel);
        drw_refresh(drw, win);
      }
      break;
    case ButtonPress:
      handle_mouse(drw, norm, sel, &ev.xbutton);
      break;
    case KeyPress: {
      KeySym ks = XLookupKeysym(&ev.xkey, 0);
      if (ks == XK_Tab) {
        show_help = !show_help;
      } else if (ks == XK_Escape) {
        if (searching) {
          searching = 0;
          memset(search_buf, 0, sizeof(search_buf));
        } else {
          exit(0);
        }
      } else if (ks == XK_slash) {
        searching = 1;
        memset(search_buf, 0, sizeof(search_buf));
        search_idx = sel_idx;
      } else if (searching) {
        handle_search(ks);
      }
      draw_list(drw, norm, sel);
      drw_refresh(drw, win);
      break;
    }
    case DestroyNotify:
      exit(0);
    }
  }
}

Window create_window(Display *dpy) {
  if (!dpy) return 0;
  int screen = DefaultScreen(dpy);
  Window root = RootWindow(dpy, screen);
  XSetWindowAttributes swa;

  int x = 0, y = 0;
  XineramaScreenInfo *info;
  int n;
  if ((info = XineramaQueryScreens(dpy, &n))) {
    XWindowAttributes wa;
    XGetWindowAttributes(dpy, root, &wa);
    x = (wa.width - win_w) / 2;
    y = (wa.height - win_h) / 2;
    XFree(info);
  }

  swa.override_redirect = True;
  swa.background_pixel = 0;
  swa.event_mask = ExposureMask | ButtonPressMask | DestroyNotify | KeyPressMask;
  Window win = XCreateWindow(dpy, root, x, y, win_w, win_h, 0, DefaultDepth(dpy, screen),
                            InputOutput, DefaultVisual(dpy, screen),
                            CWOverrideRedirect | CWBackPixel | CWEventMask, &swa);
  XMapRaised(dpy, win);
  XSetInputFocus(dpy, win, RevertToParent, CurrentTime);
  return win;
}

void usage() {
  fprintf(stderr, "Usage: command | listviewmenu [OPTIONS]\n");
  fprintf(stderr, "List view with Vim-style controls | Single-click to select\n");
  fprintf(stderr, "Examples: du -ha | listviewmenu\n");
  fprintf(stderr, "Options:\n");
  fprintf(stderr, "  -h        Show this help\n");
  fprintf(stderr, "  -w <width> Set window width (default: 1280px)\n");
  fprintf(stderr, "  -H <height> Set window height (default: 720px)\n");
  exit(0);
}

int main(int argc, char **argv) {
  for (int i = 1; i < argc; i++) {
    if (!strcmp(argv[i], "-h")) usage();
    else if (!strcmp(argv[i], "-w") && i + 1 < argc) win_w = atoi(argv[++i]);
    else if (!strcmp(argv[i], "-H") && i + 1 < argc) win_h = atoi(argv[++i]);
    else usage();
  }

  read_and_process_entries();
  if (entry_count == 0) {
    fprintf(stderr, "Error: No valid entries\n");
    fprintf(stderr, "Example: du -ha | listviewmenu\n");
    return 1;
  }

  Display *dpy = XOpenDisplay(NULL);
  if (!dpy) {
    fprintf(stderr, "Error: Cannot open X display\n");
    return 1;
  }

  int screen = DefaultScreen(dpy);
  Window root = RootWindow(dpy, screen);
  Drw *drw = drw_create(dpy, screen, root);
  if (!drw) {
    fprintf(stderr, "Error: Cannot create drawer\n");
    XCloseDisplay(dpy);
    return 1;
  }
  drw->font = font_create(drw);
  if (!drw->font) {
    fprintf(stderr, "Error: Cannot load font\n");
    XCloseDisplay(dpy);
    free(drw);
    return 1;
  }

  Clr norm[2], sel[2];
  drw_clr_create(drw, &norm[ColFg], colors[SchemeNorm][ColFg]);
  drw_clr_create(drw, &norm[ColBg], colors[SchemeNorm][ColBg]);
  drw_clr_create(drw, &sel[ColFg], colors[SchemeSel][ColFg]);
  drw_clr_create(drw, &sel[ColBg], colors[SchemeSel][ColBg]);

  Window win = create_window(dpy);
  if (!win) {
    fprintf(stderr, "Error: Cannot create window\n");
    XCloseDisplay(dpy);
    free(drw->font);
    free(drw);
    return 1;
  }

  draw_list(drw, norm, sel);
  drw_refresh(drw, win);
  event_loop(dpy, win, drw, norm, sel);

  free(entries);
  XCloseDisplay(dpy);
  free(drw->font);
  free(drw);
  return 0;
}
